package crp.core.workflow.domain.services;

import com.qwlabs.lang.C2;
import com.qwlabs.panache.Ranged;
import com.qwlabs.panache.query.PanacheQueries;
import com.qwlabs.cdi.Primary;
import crp.core.workflow.domain.events.WorkflowInstanceEnded;
import crp.core.workflow.domain.events.WorkflowInstanceStarted;
import crp.core.workflow.domain.events.WorkflowTaskEnded;
import crp.core.workflow.domain.events.WorkflowTaskStarted;
import crp.core.workflow.domain.models.ApprovalOperation;
import crp.core.workflow.domain.models.CreateWorkflowInstanceCommand;
import crp.core.workflow.domain.models.DoneWorkflowTaskCommand;
import crp.core.workflow.domain.models.WorkflowDefinitionNode;
import crp.core.workflow.domain.models.WorkflowInstanceContext;
import crp.core.workflow.domain.models.WorkflowInstanceEntity;
import crp.core.workflow.domain.models.WorkflowInstanceStatus;
import crp.core.workflow.domain.models.WorkflowTaskContext;
import crp.core.workflow.domain.models.WorkflowTaskEntity;
import crp.core.workflow.domain.models.WorkflowTaskQuery;
import crp.core.workflow.domain.models.WorkflowTaskStatus;
import crp.core.workflow.domain.repositories.WorkflowInstanceRepository;
import crp.core.workflow.domain.repositories.WorkflowTaskRepository;
import io.quarkus.panache.common.Range;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@ApplicationScoped
public class WorkflowEngine {
    private final Event<WorkflowInstanceStarted> instanceStartedEvent;
    private final Event<WorkflowInstanceEnded> instanceEndedEvent;
    private final Event<WorkflowTaskStarted> taskStartedEvent;
    private final Event<WorkflowTaskEnded> taskEndedEvent;
    private final WorkflowTaskCandidateLoader candidateLoader;
    private final WorkflowTaskRepository taskRepository;
    private final WorkflowInstanceRepository instanceRepository;
    private final WorkflowQueryConditionBuilder workflowQueryConditionBuilder;
    private final WorkflowCandidateLoader workflowCandidateLoader;


    @Inject
    public WorkflowEngine(Event<WorkflowInstanceStarted> instanceStartedEvent,
                          Event<WorkflowInstanceEnded> instanceEndedEvent,
                          Event<WorkflowTaskStarted> taskStartedEvent,
                          Event<WorkflowTaskEnded> taskEndedEvent,
                          @Primary WorkflowTaskCandidateLoader candidateLoader,
                          WorkflowTaskRepository taskRepository,
                          WorkflowInstanceRepository instanceRepository,
                          @Primary WorkflowQueryConditionBuilder workflowQueryConditionBuilder,
                          @Primary WorkflowCandidateLoader workflowCandidateLoader) {
        this.instanceStartedEvent = instanceStartedEvent;
        this.instanceEndedEvent = instanceEndedEvent;
        this.taskStartedEvent = taskStartedEvent;
        this.taskEndedEvent = taskEndedEvent;
        this.candidateLoader = candidateLoader;
        this.taskRepository = taskRepository;
        this.instanceRepository = instanceRepository;
        this.workflowQueryConditionBuilder = workflowQueryConditionBuilder;
        this.workflowCandidateLoader = workflowCandidateLoader;
    }


    public Ranged<WorkflowTaskEntity> by(WorkflowTaskQuery query, Range range) {
        Set<String> candidates = C2.set(workflowCandidateLoader.load(query.getCaller()), Object::toString);
        Set<String> instanceIds = instanceRepository.findBy(workflowQueryConditionBuilder.build(query))
                .map(WorkflowInstanceEntity::getId).collect(Collectors.toSet());
        if (C2.isEmpty(instanceIds)) {
            return Ranged.of(PanacheQueries.empty(), range);
        }
        return Ranged.of(taskRepository.findBy(candidates, instanceIds, query.getTaskStatus()), range);
    }

    public WorkflowInstanceEntity create(CreateWorkflowInstanceCommand command) {
        var context = WorkflowInstanceContext.builder()
                .externalId(command.getExternalId())
                .topic(command.getModel().getTopic())
                .submittedBy(command.getSubmittedBy())
                .build();
        WorkflowInstanceEntity entity = new WorkflowInstanceEntity();
        entity.setModel(command.getModel());
        entity.setContext(context);
        entity.setStatus(WorkflowInstanceStatus.RUNNING);
        instanceRepository.persistAndFlush(entity);
        instanceStartedEvent.fire(WorkflowInstanceStarted.builder()
                .instanceId(entity.getId())
                .context(context)
                .build());
        return entity;
    }

    public Optional<WorkflowTaskEntity> nextTask(WorkflowInstanceEntity instance) {
        return nextTask(instance, null);
    }

    public Optional<WorkflowTaskEntity> nextTask(WorkflowInstanceEntity instance, WorkflowTaskEntity task) {
        Optional<WorkflowDefinitionNode> mayNextNode = nextTaskNode(instance, task);
        if (mayNextNode.isEmpty()) {
            instanceEndedEvent.fire(WorkflowInstanceEnded.builder()
                    .instanceId(instance.getId())
                    .context(instance.getContext())
                    .build());
            return Optional.empty();
        }
        return nextTask(instance, task, mayNextNode.get());
    }

    private Optional<WorkflowTaskEntity> nextTask(WorkflowInstanceEntity instance,
                                                  WorkflowTaskEntity task,
                                                  WorkflowDefinitionNode nextNode) {
        var topic = instance.getModel().getTopic();
        var candidates = candidateLoader.load(WorkflowCandidateLoadContext.builder()
                .instance(instance)
                .node(nextNode)
                .build());
        WorkflowTaskEntity nextTask = new WorkflowTaskEntity();
        nextTask.setInstance(instance);
        nextTask.setContext(WorkflowTaskContext.builder()
                .topic(topic)
                .nodeId(nextNode.getId())
                .autocomplete(nextNode.isAutocomplete())
                .build());
        nextTask.setStatus(WorkflowTaskStatus.PENDING);
        nextTask.setCandidates(C2.set(candidates, Objects::toString).toArray(new String[]{}));
        taskRepository.persistAndFlush(nextTask);
        taskStartedEvent.fire(WorkflowTaskStarted.builder()
                .instance(instance)
                .previousTask(task)
                .task(nextTask)
                .build());
        return Optional.of(nextTask);
    }

    public void doneTask(DoneWorkflowTaskCommand command) {
        var task = doDoneTask(command);
        if (command.getNextNode().isEmpty()) {
            instanceEndedEvent.fire(WorkflowInstanceEnded.builder()
                    .instanceId(command.getTask().getInstance().getId())
                    .context(command.getTask().getInstance().getContext())
                    .build());
            return;
        }
        nextTask(task.getInstance(), task, command.getNextNode().get());
    }

    private WorkflowTaskEntity doDoneTask(DoneWorkflowTaskCommand command) {
        var task = command.getTask();
        var context = task.getContext();
        context.setRawFormAttributes(command.getFormAttributes());
        context.setComment(command.getComment());
        task.setOperation(command.getOperation());
        task.setStatus(WorkflowTaskStatus.DONE);
        taskRepository.persistAndFlush(task);
        taskEndedEvent.fire(WorkflowTaskEnded.builder()
                .instance(task.getInstance())
                .task(task)
                .build());
        return task;
    }

    public Optional<WorkflowDefinitionNode> nextTaskNode(WorkflowInstanceEntity instance, WorkflowTaskEntity task) {
        var currentNodeId = Optional.ofNullable(task)
                .map(WorkflowTaskEntity::getContext)
                .map(WorkflowTaskContext::getNodeId)
                .orElse(null);
        return instance.getModel().getDefinition().next(currentNodeId);
    }

    public void autoDoneTask(WorkflowInstanceEntity instance, WorkflowTaskEntity task) {
        var command = DoneWorkflowTaskCommand.builder()
                .task(task)
                .nextNode(nextTaskNode(instance, task))
                .operation(ApprovalOperation.PASSED)
                .build();
        doneTask(command);
    }
}
